接下來,我們需要考慮到 狀態(state) 這個概念。在 Streamlit 中,許多之後要時做的 Component(例如Button、Textbox)都需要狀態資訊。
State 的主要功能是將 Component 的 ID 與其對應的值進行關聯。由於不同元件可能儲存不同類型的值,因此我們將值的型別設定為 any。
為了確保多個執行緒同時存取 State 時不會發生數據衝突,我們使用了 sync.RWMutex
來保護 State。
Thread Safe 是一個複雜的問題。即使使用了 RWMutex,仍然可能存在一個重要的問題:Script 可能會修改 State 中的值,這可能導致預期外的行為。例如,一個腳本可能會不小心覆蓋了另一個腳本設定的值。
這些問題需要我們在 Service 使用 State 時仔細考慮。我們先將這些問題保留下來,後面再深入探討解決方案。
package tgstate
import "sync"
type State struct {
data map[string]any
lock sync.RWMutex
}
func NewState() *State {
return &State{
data: map[string]any{},
}
}
func (s *State) Set(key string, val any) {
s.lock.Lock()
defer s.lock.Unlock()
s.data[key] = val
}
func (s *State) Del(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.data, key)
}
func (s *State) Get(key string) any {
s.lock.RLock()
defer s.lock.RUnlock()
return s.data[key]
}
func (s *State) GetString(key string) string {
v := s.Get(key)
if v == nil {
return ""
} else {
return v.(string)
}
}
func (s *State) GetBool(key string) bool {
v := s.Get(key)
if v == nil {
return false
} else {
return v.(bool)
}
}
State Table 用於管理多個 State。除了維護 State 與其 ID 之間的對應關係外,它還提供了一種垃圾回收機制。我們採用生存時間(TTL)的方式,定期清理那些長時間未被使用的 State。
垃圾回收機制:
package tgstate
import (
"sync"
"time"
"github.com/google/uuid"
)
type stateData struct {
state *State
timestamp time.Time
}
type StateTable struct {
table map[string]*stateData
lock sync.RWMutex
wg sync.WaitGroup
stop chan bool
stateTTL time.Duration
clearCycle time.Duration
}
func NewStateTable(stateTTL, clearCycle time.Duration) *StateTable {
return &StateTable{
table: map[string]*stateData{},
stop: make(chan bool),
stateTTL: stateTTL,
clearCycle: clearCycle,
}
}
func (st *StateTable) Start() {
st.wg.Add(1)
go func() {
defer st.wg.Done()
ticker := time.NewTicker(st.clearCycle)
defer ticker.Stop()
for {
select {
case <-ticker.C:
st.gc()
case <-st.stop:
return
}
}
}()
}
func (st *StateTable) Stop() {
st.stop <- true
st.wg.Wait()
}
func (st *StateTable) gc() {
st.lock.Lock()
defer st.lock.Unlock()
rmKeys := []string{}
for key, data := range st.table {
if time.Since(data.timestamp) >= st.stateTTL {
rmKeys = append(rmKeys, key)
}
}
for _, key := range rmKeys {
delete(st.table, key)
}
}
func (st *StateTable) Get(id string) *State {
st.lock.RLock()
defer st.lock.RUnlock()
d := st.table[id]
if d == nil {
return nil
}
d.timestamp = time.Now()
return d.state
}
func (st *StateTable) NewState() (*State, string) {
st.lock.Lock()
defer st.lock.Unlock()
var newID string
for {
newID := uuid.New().String()
if st.table[newID] == nil {
break
}
}
state := NewState()
st.table[newID] = &stateData{
state: state,
timestamp: time.Now(),
}
return state, newID
}